/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.generator;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import redora.generator.Template.Destination;
import redora.generator.Template.Type;
import redora.generator.exceptions.ModelGenerationException;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.core.ParseException;
import freemarker.ext.dom.NodeModel;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;

/**
 * Wrapper for Freemarker template engine. It generates files on a given
 * Template. Part of this class are the several directories where for example
 * the generated source files are located.
 * 
 * @see Templates
 *@author jaap
 */
public class ModelFileHandler {
    /** project build directory, default target */
    private final String buildDir;
    /** project source directory, default src/main/java */
    private final String sourceDir;
    /** project resource directory, default src/main/resources */
    private final String resourceDir;
    private Configuration freemarkerConf;

    private TemplateHandler tHandler;

    /**
     * Initializes and sets the Freemarker (caching) configuration. This also
     * means that Freemarker templates can be located in the usual location (the
     * root of the classpath), a custom location (in /templates of the
     * classpath) and locally in the p-templates/resources for use in this
     * project.
     * 
     * @param sourceDir
     * @param buildDir
     * @param resourceDir
     * @throws ModelGenerationException
     */
    public ModelFileHandler(String sourceDir, String buildDir, String resourceDir)
            throws ModelGenerationException {
        this.sourceDir = sourceDir;
        this.buildDir = buildDir;
        this.resourceDir = resourceDir;
        try {
            this.tHandler = new TemplateHandler();
        } catch (Exception e) {
            throw new ModelGenerationException("Failed to create TemplateHandler", e);
        }
        if (findModelFiles().isEmpty())
            throw new ModelGenerationException("Cannot find any model in " + resourceDir);

        // Prepare Freemarker
        freemarkerConf = new Configuration();
        ClassTemplateLoader custom = new ClassTemplateLoader(getClass(), "/templates");
        ClassTemplateLoader standard = new ClassTemplateLoader(getClass(), "/");
        File tplDir = new File(".." + File.separator + "p-templates" + File.separator
                + resourcesDir());
        if (!tplDir.exists()) {
            tplDir = new File(localTemplatesDir());
        }
        try {
            FileTemplateLoader local = new FileTemplateLoader(tplDir);
            freemarkerConf.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[] { local,
                    custom, standard }));
        } catch (IOException e) {
            freemarkerConf.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[] { custom,
                    standard }));
        }
        freemarkerConf.setTemplateUpdateDelay(9000);
        //freemarkerConf.addAutoInclude("includeMacro.ftl");
    }

    /**
     * src/main/resources/
     */
    public final String resourcesDir() {
        return resourceDir + File.separator;
    }

    /**
     * src/main/resources/model/
     */
    public final String modelDir() {
        return resourcesDir() + "model" + File.separator;
    }

    /**
     * src/main/resources/templates/
     */
    public final String localTemplatesDir() {
        return resourcesDir() + "templates" + File.separator;
    }

    /**
     * src/main/resources/model/includes/
     */
    public final String includeDir() {
        return modelDir() + "includes" + File.separator;
    }

    /**
     * Creates the generated source file based on given template and model
     * document.
     */
    public void process(Template tpl, Document model, String packageName, String outFileName, HashMap<String, String> param)
            throws ModelGenerationException, SAXException, ParserConfigurationException, IOException {
        System.out.print("Processing with " + tpl.getTemplateFileName());

        String destinationPath;
        if (tpl.destination == Destination.target) {
            if (tpl.path == null) {
                destinationPath = buildDir + File.separator + "generated-sources" + File.separator
                        + GenerateMojo.GENERATION_TARGET + File.separator;
            } else {
                destinationPath = buildDir + File.separator;
            }
        } else {
            if (tpl.path == null) {
                destinationPath = sourceDir + File.separator;
            } else {
                destinationPath = "src" + File.separator;
            }
        }
        if (tpl.path == null) {
            destinationPath += packageToPath(packageName);
        } else {
            destinationPath += tpl.path.replace("/", File.separator).replace("\\", File.separator)
                    + File.separator;
        }
        System.out.println(" to " + destinationPath + outFileName);

        if (tpl.destination == Destination.source) {
            if (hasFile(outFileName, destinationPath)) {
                System.out.println("Stub " + outFileName + " already exists.");
                return;
            }
        }

        // -------> Prepare Output
        createPath(destinationPath);
        Writer out;
        try {
            out = new OutputStreamWriter(new FileOutputStream(destinationPath + outFileName));
        } catch (FileNotFoundException e) {
            throw new ModelGenerationException("Can't find: " + destinationPath + outFileName, e);
        }

        if (tpl.type == Type.freemarker) {
            // Input ----->
            Map<String, NodeModel> root = new HashMap<String, NodeModel>();
            root.put("doc", NodeModel.wrap(model));

            try {
                freemarker.template.Template template = freemarkerConf.getTemplate(tpl
                        .getTemplateFileName());
                template.process(root, out);
            } catch (ParseException e) {
                throw new ModelGenerationException(
                        "There is a error in template: " + tpl.getTemplateFileName()
                                + ". I found it when generating " + outFileName, e);
            } catch (IOException e) {
                throw new ModelGenerationException("Can't find '" + tpl.getTemplateFileName()
                        + "' when generating " + outFileName, e);
            } catch (TemplateException e) {
                throw new ModelGenerationException(
                        "There is a error in template: " + tpl.getTemplateFileName()
                                + ". I found it when generating " + outFileName, e);
            } catch (RuntimeException e) {
                throw new ModelGenerationException(
                        "There is another error while trying this template: "
                                + tpl.getTemplateFileName() + ". I found it when generating "
                                + outFileName, e);
            }
        } else if (tpl.type == Type.xslt) {
            try {
                XMLUtil.xsltTransform(model.getFirstChild(), tHandler.findFile(tpl
                        .getTemplateFileName()), out, param);
            } catch (TransformerException e) {
                throw new ModelGenerationException("Sorry, i failed to use this template: "
                        + tpl.getTemplateFileName() + ". It broke when generating " + outFileName,
                        e);
            }
        } else if (tpl.type == Type.copy) {
            IOUtils.copy(tHandler.findFile(tpl.getTemplateFileName()), out);
            out.flush();
        }
    }

    /**
     * Create path for generated-sources if the path is not existed.
     * */
    public static void createPath(String path) {
        File dir = new File(path);
        if (!dir.exists()) {
            System.out.println("Created path: " + dir.getPath() + " (" + dir.mkdirs() + ")");
        }
    }

    /**
     * Get all model documents file list
     * 
     * @return All "???.xml" files in the model directory (no subdiretories)
     */
    public Set<String> findModelFiles() {
        Set<String> retVal = new HashSet<String>();
        File dir = new File(modelDir());
        String[] model = dir.list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".xml");
            }
        });

        if (model != null && model.length > 0)
            Collections.addAll(retVal, model);

        return retVal;
    }

    /**
     * Get all include documents file list
     * 
     * @return All "???.xml" files in the include directory (no subdiretories)
     */
    public Set<String> findIncludeFiles() {
        Set<String> retVal = new HashSet<String>();
        File dir = new File(includeDir());
        String[] model = dir.list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".xml");
            }
        });

        if (model != null && model.length > 0)
            Collections.addAll(retVal, model);
        return retVal;
    }

    /** Check if the source file exists. */
    public boolean hasFile(String fileName, String path) {
        return new File(path + File.separator + fileName).exists();
    }

    /**
     * Convert the package to the real file path
     * */
    public static String packageToPath(String packageName) {
        StringTokenizer pathToken = new StringTokenizer(packageName, ".");
        String retVal = "";
        while (pathToken.hasMoreElements()) {
            retVal += pathToken.nextToken();
            retVal += File.separator;
        }

        return retVal;
    }
}
